「恩,就是這麼一回事了。我漸漸離我的抱負越來越遠,漸漸在她的愛中沉的越來越深,然後突然之間我什麼都不在意了。在應用程式當中觸發各種事件又有什麼用呢?還不如好好地跳一些通知給她,告訴她我又觸發了哪些事件。」
~節錄自《The Great Svelte:第八章》
App.svelte
取得 toastStore
的資料Palettes.svelte
取得 toastStore
的資料經過兩天的介紹,對於 Svelte 提供的 Store 物件應該有初步的概念了。今天就讓我們試著在色票產生器這個專案當中來使用 Store,一邊練習一邊加深我們對 Store 的理解吧!
一開始在介紹 Store 的時候,我們說 Store 可以實作出水平傳遞的資料,讓沒有互相隸屬的兩個 Svelte 元件仍然可以透過 Store 來溝通。什麼情況會需要用到這種設計呢?在我們現在色票產生器這個專案裡頭,如果想要實作出一個訊息中心,每當使用者在互動介面上做出新的操作,就跳出訊息來提示使用者。這樣子的一個訊息中心,需要從各個不同的 Svelte 元件獲取事件發生的詳細情況,也就是需要與各種不同的 Svelte 元件進行資料溝通。所以如果想要實作出訊息中心,看起來就適合使用 Store 所提供的水平傳遞資料的特性,做出一個 Store 物件來儲存所有的訊息,並且讓各個不同的 Svelte 都能將各自發生的事件直接傳達到這個 Store 物件當中。
在現代的網頁設計當中,一則一則隨著事件發生而跳出來提示使用者的訊息有個可愛的暱稱,叫做 Toast。所以我們就來做個 Toast Store 吧。首先在 /src/lib
當中開啟一個新的 Javascript 檔案 toastStore
:
/src/lib/toastStore.js
import { writable } from "svelte/store";
const createToastStore = () => {
let uuid = 0;
const { subscribe, update } = writable([]);
return {
subscribe,
addToast: ({ action, hex }) => {
const id = uuid++;
update(state => [...state, { action, hex, id: id }])
setTimeout(() => update(state => state.filter(x => x.id !== id)), 5000)
},
removeToast: ({ id }) => update(state => state.filter(x => x.id !== id)),
}
}
export const toastStore = createToastStore();
第一行:import { writable } from "svelte/store";
先引入 writable
這個 Svelte 所提供最基礎萬用的 Store。
第三行:const createToastStore = () => {
宣告一個函式,createToastStore
,讓我們從 writable
這個最基礎的 Store 出發,並包裝成一個適合當作訊息中心的 toast Store。
第四行:let uuid = 0;
宣告一個變數 uuid
,這個變數將會替每一則新加入的訊息標上識別碼。
第五行:const { subscribe, update } = writable([]);
初始化一個 writable
Store。並從中取出我們需要的方法 subscribe
跟 update
。
第七行:return {
回傳一個物件。這個物件將會提供客製化的方式來更新我們的 writable
Store。
第八行:subscribe,
首先原封不動的回傳 subscribe
這個方法。
第九行:addToast: ({ action, hex }) => {
並且實作出一個新方法 addToast
。這個新的方法會需要一個具有 action
跟 hex
兩種資料的物件當作參數,執行的時候會將識別碼 uuid
增加一,藉此達到獨一無二的效果。
第十一行:update(state => [...state, { action, hex, id: id }])
接著利用 update
將我們的 writable
物件內的資料做更新。更新的方法是加入一個具有 action
、hex
、以及 id
這三種資料的物件。
第十二行:setTimeout(() => update(state => state.filter(x => x.id !== id)), 5000)
我們不希望訊息中心跳出來的提示一直顯示在畫面當中,所以經過 5000
毫秒之後,利用 setTimeout
將這個訊息又從 writable
的物件中刪除。
第十四行:removeToast: ({ id }) => update(state => state.filter(x => x.id !== id)),
實作一個新方法 removeToast
。這個方法會需要一個具有 id
資料的物件作為參數,並且依此刪掉 writable
訊息中心相對應的那一則訊息。
第十八行:export const toastStore = createToastStore();
執行 createToastStore
。這樣一來就會得到一個具有 subscribe
、addToast
、removeToast
這三種方法的物件。由於這三種方法是源自於 writable
Store 的 subscribe
跟 update
變化而來,所以這個新的物件也是一種 Store(其實就是一個包裝過的 writable
Store)。而這個新的 Store 當中所儲存的變數會是一個陣列,陣列當中一則一則的物件,具有 action
、hex
、跟 id
,就是我們想要呈現的訊息跟每個訊息的詳細資料。我們把這個新種類的 Store 命名為 toastStore
,並且用 export
關鍵字讓其他檔案也能使用。
沒錯就是這麼簡單,短短十八行就可以做出一間美味可口的 toastStore.js
!
並且做一個 Svelte 元件 Toast.svelte
來呈現我們美味的 toast:
/src/lib/Toast.svelte
<script>
import { toastStore } from "./toastStore";
import plus from "../assets/plus.svg";
import minus from "../assets/minus.svg";
import lock from "../assets/lock.svg";
import unlock from "../assets/unlock.svg";
export let toast;
const icons = { plus, minus, lock, unlock };
$: icon = icons[toast.action];
</script>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div class="toast" on:click={() => toastStore.removeToast({ id: toast.id })}>
<div class="icon">
<img src={icon} alt={toast.action} />
</div>
<div class="hex" style="color: #{toast.hex}">{toast.hex}</div>
</div>
<!-- CSS的部分先略過不呈現,詳細可見文末附錄 -->
第二行:import { toastStore } from "./toastStore";
引入我們做好的 toastStore
。
第七行:export let toast;
宣告一個 Toast.svelte
的 Property toast
。
第十四行:<div class="toast" on:click={() => toastStore.removeToast({ id: toast.id })}>
這就是我們一則一則 toast 在 HTML 上面呈現出來的本體。加上一個 on:click
事件處理器,讓使用者可以透過點擊 toast 本體直接點掉這個提示訊息。
App.svelte
取得 toastStore
的資料 既然有了 toastStore
這個美味的 Store,也有了用來呈現 toastStore
資料的 Svelte 元件 Toast.svelte
,那麼我們就來到 App.svelte
使用看看吧:
/src/App.svelte
<script>
/* 省略與今天主題無關的內容 */
import Toast from "./lib/Toast.svelte";
import { toastStore } from "./lib/toastStore";
const handleClick = (e) => {
console.log(e);
const tobeCount = count + e.detail;
if (0 <= tobeCount && tobeCount < 7) {
switch (e.detail) {
case 1:
palettes = [...palettes, generatePalette()];
toastStore.addToast({
action: "plus",
hex: palettes.slice(-1)[0].hex,
});
break;
case -1:
toastStore.addToast({
action: "minus",
hex: palettes.slice(-1)[0].hex,
});
palettes = palettes.slice(0, -1);
break;
}
} else showModal = true;
};
</script>
<!-- 將 Toast 放在這邊 -->
{#each $toastStore as toast}
<Toast {toast} />
{/each}
<main>
<!-- 省略與今天主題無關的內容 -->
</main>
<Modal {showModal} on:closeModal={() => (showModal = false)} />
第三行:import Toast from "./lib/Toast.svelte";
引入需要的 Svelte 元件 Toast.svelte
。
第四行:import { toastStore } from "./lib/toastStore";
從 Javascript 檔案 toastStore.js
當中引入 toastStore
這個 Store 物件。
第十二行:palettes = [...palettes, generatePalette()];
如果 <Counter />
當中代表加一的事件發生時,替我們的 palettes
多加入一個色票。
第十三行: toastStore.addToast({
並且用 addToast
這個方法,將發生的事件 action: "plus"
跟新加入的色碼 hex: palettes.slice(-1)[0].hex
包裝起來儲存到 toastStore
這個 Store 裡面的陣列當中。也就是新增一個「色票加一」的通知。
第十九行:toastStore.addToast({
如果 <Counter />
當中代表減一的事件發生時,先將即將發生的事件 action: "minus"
跟準備要被移除的色碼 hex: palettes.slice(-1)[0].hex
包裝起來儲存到 toastStore
這個 Store 裡面的陣列當中。也就是新增一個「色票減一」的通知。
第二十三行:palettes = palettes.slice(0, -1);
然後就將色票減一。
第三十一行:{#each $toastStore as toast}
利用第 25 天:Svelte 的互動百寶箱:Store(二)介紹過的自動訂閱 (auto-subscription) 的作法,直接用 $toastStore
取得 toastStore
當中變數的值。因為這個變數是一個陣列,所以使用 {#each}
展開 each
邏輯區塊來迭代陣列當中的各個項目,也就是各個訊息。
第三十二行:<Toast {toast} />
每一則訊息就做一個 <Toast />
,並且將訊息的詳細內容用 toast
Property 傳遞給 <Toast />
。
第三十三行:{/each}
結束 each
邏輯區塊。
圖一、一則一則跳出來的可愛 toast
以往 Svelte 元件間要互相傳遞資料只能透過 Property,因此在設計檔案架構時,需要共用相同資料的 Svelte 元件必為從屬關係,讓資料可以從父元件 (parent component) 向子元件 (child component) 傳遞,或是透過 bind
讓子元件 (child component) 去修改父元件 (parent) 的資料。現在有了 Store,需要共用相同資料很簡單,只要在 Javascript 的段落中用 import
直接將需要的 Store 引入進來就可以。
Palettes.svelte
取得 toastStore
的資料 除了在 App.svelte
可以直接獲取並更新 toastStore
當中儲存的變數之外,讓我們也試試看從 Palettes.svelte
來對 toastStore
儲存的資料做改變:
/src/lib/Palettes.svelte
<script>
import unlock from "../assets/unlock.svg";
import lock from "../assets/lock.svg";
import { toastStore } from "./toastStore";
export let palettes;
/* 省略與今天主題無關的內容 */
const handleLock = ({ locked, hex }) =>
toastStore.addToast({ action: locked ? "lock" : "unlock", hex });
</script>
<div class="palettes">
<!-- 省略與今天主題無關的內容 -->
<div class="lock-icon">
<label>
<input
type="checkbox"
bind:checked={locked}
on:change={() => handleLock({ locked, hex })}
/>
{#if locked}
<img src={lock} alt="color-locked" />
{:else}
<img src={unlock} alt="color-unlocked" />
{/if}
</label>
</div>
<!-- 省略與今天主題無關的內容 -->
</div>
第四行:import { toastStore } from "./toastStore";
從 toastStore.js
當中引入需要的 toastStore
。
第八行:const handleLock = ({ locked, hex }) =>
宣告一個事件處理器 handleLock
,用來處理上鎖/解鎖的事件。雖然我們已經用 bind
讓主要元件 App.svelte
當中的變數 palettes
可以直接做改變了,但現在除了 palettes
需要做改變之外,我們也需要改變 toastStore
當中的變數。所以這邊多實作一個事件處理器去完成這項任務。
第九行:toastStore.addToast({ action: locked ? "lock" : "unlock", hex });
事件處理器的工作很簡單,當更新之後的 locked
狀態為 true
,就用 addToast
這個方法新增一個「色票上鎖」的通知。反之,當更新之後的 locked
狀態為 false
,就用 addToast
這個方法新增一個「色票解鎖」的通知。
第二十行:on:change={() => handleLock({ locked, hex })}
並在 <input type="checkbox">
這個 HTML 元素上放入我們需要的事件處理器 handleLock
。
圖二、我鎖起來了!我又解鎖了!我又鎖起來了!我又又解鎖了!
那麼這就是今天關於 Store 的實作部分了,詳細的程式碼可以參考文末附錄,或是在 Github 資源庫上也可以找到。謝謝大家。
Toast.svelte
/src/lib/Toast.svelte
<script>
import { toastStore } from "./toastStore";
import plus from "../assets/plus.svg";
import minus from "../assets/minus.svg";
import lock from "../assets/lock.svg";
import unlock from "../assets/unlock.svg";
export let toast;
const icons = { plus, minus, lock, unlock };
$: icon = icons[toast.action];
</script>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div class="toast" on:click={() => toastStore.removeToast({ id: toast.id })}>
<div class="icon">
<img src={icon} alt={toast.action} />
</div>
<div class="hex" style="color: #{toast.hex}">{toast.hex}</div>
</div>
<style>
.toast {
font-size: 1.2em;
font-weight: bold;
width: 10em;
padding: 1em 0;
background: #fff;
display: flex;
justify-content: center;
border-radius: 0.5em;
gap: 1em;
position: fixed;
bottom: 1em;
left: 50%;
transform: translateX(-50%);
}
.toast .icon {
width: 1.2em;
}
.toast .hex {
width: 5em;
}
</style>